/*******************************************************************************
 * Copyright 2010 Prometheus Consulting
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package nz.co.senanque.madura.bundle;

import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

/**
 * This class manages multiple bundles. Each bundle is a jar file loaded under a different classloader.
 * The different bundles each contain a BundleRoot, which is all we know about them apart from their file name.
 * There is an injected directory that is scanned on startup and at other times for jar files.
 * Any new bundles found are loaded. Any that were found earlier and are gone are shut down.
 * When the manager is shut down all bundles are shut down as well.
 * 
 * @author Roger Parkinson
 * @version $Revision: 1.5 $
 */
public class BundleManagerImpl implements BundleManager
{
    private Logger m_logger = LoggerFactory.getLogger(this.getClass());
    private Map<String,BundleRoot> m_bundleMap = new HashMap<String,BundleRoot>();
    private String m_directory;
    private ThreadLocal<String> m_currentBundle = new ThreadLocal<String>();
    private boolean m_lock = false;
    
    public void init()
    {
        scan();
    }
    public synchronized void scan()
    {
        File dir = new File(m_directory);
        File[] jarFiles = dir.listFiles(new FilenameFilter(){

            public boolean accept(File arg0, String arg1)
            {
                if (arg1.toUpperCase().endsWith("JAR")) return true;
                return false;
            }});
        
        // Put all the names in the list of delete candidates
        // we will remove the ones we want to keep
        List<String> deleteCandidates = new ArrayList<String>();
        for (String bundleName:m_bundleMap.keySet())
        {
            deleteCandidates.add(bundleName);
        }
        
        m_lock = true;
        // Scan all the files in the directory
        for (File file:jarFiles)
        {
            String fileName = file.getName().toLowerCase();
            int i = fileName.lastIndexOf(".jar");
            String bundleName = fileName.substring(0,i);
            BundleRoot root = m_bundleMap.get(bundleName);
            if (root == null)
            {
                // This is a new one so add it
                addBundle(bundleName, file);
            }
            else
            {
                // Make sure we don't remove this
                deleteCandidates.remove(bundleName);
                if (root.getDate() != file.lastModified())
                {
                    m_bundleMap.remove(root);
                    addBundle(bundleName, file);
                }
            }
        }
        for (String bundleName:deleteCandidates)
        {
            deleteBundle(bundleName);
        }
        m_lock = false;
    }
    private void deleteBundle(String bundleName)
    {
        BundleRoot root = m_bundleMap.get(bundleName);
        root.shutdown();
        m_bundleMap.remove(bundleName);
    }
    @SuppressWarnings("unchecked")
    private void addBundle(String bundleName, File file)
    {
        try
        {
            JarFile jar = new JarFile(file);
            Manifest mf = jar.getManifest();
            Attributes attributes = mf.getMainAttributes();
            Properties properties = new Properties();
            for (Map.Entry<Object,Object> a :attributes.entrySet())
            {
                Object key = a.getKey();
                Object value = a.getValue();
                properties.setProperty(key.toString(), value.toString());
                m_logger.debug("Property: {} value {}",key.toString(), value.toString());
            }
            properties.setProperty("bundle.name", 
                    figureBundleName(
                            String.valueOf(properties.get("Bundle-Name")),
                            String.valueOf(properties.get("Bundle-Version"))));
            String className = attributes.getValue("Bundle-Activator");
            URL[] urls = new URL[]{file.toURI().toURL()};
            ClassLoader classLoader = new URLClassLoader(urls, this.getClass().getClassLoader());
            Class<BundleRoot> clazz = (Class<BundleRoot>)classLoader.loadClass(className);
            BundleRoot root = (BundleRoot)clazz.newInstance();
            root.setDate(file.lastModified());
            root.init(properties);
            m_bundleMap.put(bundleName, root);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
    public void shutdown()
    {
        
    }
    public BundleRoot getBundle()
    {
        // Fairly crude locking mechanism to ensure that the 
        // the method waits if the bundle list is being worked on
        // We don't want to take out a lock here because there's not need to lock out other getters
        while (m_lock)
        {
            try
            {
                Thread.sleep(500);
            }
            catch (InterruptedException e)
            {
            }
        }
        return m_bundleMap.get(m_currentBundle.get());
    }
    public void setBundle(String bundleName, String version)
    {
        m_currentBundle.set(selectBestBundle(bundleName,version));
    }
    public void setBundle(String bundleName)
    {
        m_currentBundle.set(selectBestBundle(bundleName,null));
    }
    public String getDirectory()
    {
        return m_directory;
    }
    private String selectBestBundle(String bundle, String version)
    {
        bundle = figureBundleName(bundle,version);
        String previousBundle = m_bundleMap.get(bundle) != null?bundle:null;
        if (previousBundle != null)
            return previousBundle;
        for (String key : m_bundleMap.keySet())
        {
            if (bundle.compareTo(key)>0)
                return previousBundle;
            previousBundle = key;
        }
        if (previousBundle == null)
            throw new RuntimeException("No bundle found for "+bundle);
        return previousBundle;
    }
    @Required
    public void setDirectory(String directory)
    {
        m_directory = directory;
    }
    private String figureBundleName(String bundle, String version)
    {
        if (version != null)
            bundle += "-"+version;
        return bundle;
    }

}
